package cn.lg.soar.system.account.service.impl;

import cn.lg.soar.common.data.msg.I18n;
import cn.lg.soar.common.enums.DeviceStatusEnum;
import cn.lg.soar.common.util.AssertUtil;
import cn.lg.soar.common.util.DatetimeUtil;
import cn.lg.soar.common.util.UrlUtils;
import cn.lg.soar.common.util.Validator;
import cn.lg.soar.common.util.current.CurrentProxy;
import cn.lg.soar.common.util.data.DataUtil;
import cn.lg.soar.common.util.data.StringUtil;
import cn.lg.soar.common.util.token.SoarJWT;
import cn.lg.soar.core.util.InnerSerializeUtils;
import cn.lg.soar.mvc.util.RequestUtils;
import cn.lg.soar.mvc.util.ServletUtils;
import cn.lg.soar.system.account.model.*;
import cn.lg.soar.system.account.service.ILoginService;
import cn.lg.soar.system.account.service.ISsoServerService;
import cn.lg.soar.system.api.constant.ModuleConst;
import cn.lg.soar.system.auth.entity.SsoApply;
import cn.lg.soar.system.auth.entity.User;
import cn.lg.soar.system.auth.service.ISsoApplyService;
import cn.lg.soar.system.auth.service.IUserService;
import cn.lg.soar.system.config.SsoServerProps;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import eu.bitwalker.useragentutils.DeviceType;
import eu.bitwalker.useragentutils.UserAgent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;

/**
 * 单点登录服务端
 * @author luguoxiang
 * @date 2022/5/28
 * 开源项目：https://gitee.com/lgx1992/lg-soar 求star！请给我star！请帮我点个star！
 */
@ConditionalOnProperty(name = "lg.sso.server.enabled", havingValue = "true")
public class SsoServerServiceImpl implements ISsoServerService, CurrentProxy<SsoServerServiceImpl> {
    private static final Logger LOGGER = LoggerFactory.getLogger(SsoServerServiceImpl.class);
    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private ISsoApplyService clientService;
    @Autowired
    private IUserService userService;
    @Autowired
    private ILoginService loginService;

    private final SoarJWT soarJWT;
    private final Duration expire;

    public SsoServerServiceImpl(SsoServerProps props) {
        if (Boolean.TRUE.equals(props.getEnabled())) {
            this.expire = props.getExpire();
            this.soarJWT = new SoarJWT(props.getSecret(), this.expire);
        } else {
            this.expire = Duration.ZERO;
            this.soarJWT = new SoarJWT("", this.expire);
        }
    }

    @Override
    public SsoServerInfo online(SsoClientInfo clientInfo) {
        Long clientId = clientInfo.getClientId();
        String signValue = clientInfo.getSignValue();
        AssertUtil.notNull(clientId, "缺少clientId");
        AssertUtil.notEmpty(signValue, "缺少签名值");
        // 读取客户端数据
        SsoApply client = clientService.getById(clientId);
        AssertUtil.notNull(client, "客户端不存在");
        String secret = client.getSecret();
        // 验签
        if (!clientInfo.verification(secret)) {
            LOGGER.warn("客户端上线失败，疑似攻击！" + InnerSerializeUtils.toJson(clientInfo));
            AssertUtil.throwException(I18n.build("验签失败"));
        }
        // 客户端访问次数限制
        Integer count = client.getOnlineCount();
        LocalDateTime dateTime;
        if (count == null) {
            count = 0;
            dateTime = DatetimeUtil.last(ChronoUnit.DAYS);
        } else {
            if (count >= 200) {
                if (client.getOnlineCountExpire().isAfter(LocalDateTime.now())) {
                    AssertUtil.throwException(I18n.build("调用次数已达上限：{0}次/日"), 200);
                }

                count = 0;
                dateTime = DatetimeUtil.last(ChronoUnit.DAYS);
            } else {
                dateTime = client.getOnlineCountExpire() == null ? DatetimeUtil.last(ChronoUnit.DAYS) : null;
            }
        }

        // 获取token
        String token = getToken(clientId);
        SsoServerInfo serverInfo = new SsoServerInfo();
        // 赋值
        serverInfo.setValidationUri(ModuleConst.MODULE_NAME + "/sso/server/validation");
        serverInfo.setLogoutNotifyUri(ModuleConst.MODULE_NAME + "/sso/server/logout-notify");
        serverInfo.setOfflineUri(ModuleConst.MODULE_NAME + "/sso/server/offline");
        serverInfo.setSsoToken(token);
        serverInfo.signature(secret);
        // 更新客户端状态
        clientService.update(
                Wrappers.<SsoApply>lambdaUpdate()
                        .set(SsoApply::getStatus, DeviceStatusEnum.normal.getValue())
                        .set(Validator.isUrl(clientInfo.getLogoutNotifyUrl()),
                                SsoApply::getLogoutNotifyUrl, clientInfo.getLogoutNotifyUrl())
                        .set(Validator.isUrl(clientInfo.getHomeUrl()),
                                SsoApply::getHomeUrl, clientInfo.getHomeUrl())
                        .set(SsoApply::getOnlineId, clientInfo.getServerId())
                        .set(SsoApply::getOnlineCount, count + 1)
                        .set(dateTime != null, SsoApply::getOnlineCountExpire, dateTime)
                        .eq(SsoApply::getId, client.getId())
        );
        clientService.removeCache(clientId);
        LOGGER.info("客户端上线：" + clientId);
        return serverInfo;
    }


    @Override
    public String getTicket(Long userId) {
        // 获取客户端设备类型
        HttpServletRequest request = ServletUtils.getRequest();
        UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
        DeviceType deviceType = userAgent.getOperatingSystem().getDeviceType();
        return soarJWT.create(new Object[]{ userId, deviceType.ordinal() });
    }

    @Override
    public SsoUser validation(SsoValidation validation) {
        // 验证token
        parseToken(validation.getSsoToken());
        // 验证ticket
        SoarJWT.Result result = soarJWT.verify(validation.getTicket());
        AssertUtil.isTrue(result.isValid(), "sso ticket 无效");
        Object[] payload = result.getPayload(Object[].class);
        AssertUtil.notEmpty(payload, "sso ticket 载体无效");
        AssertUtil.notEmpty(payload[0], "sso ticket userId 无效");
        // 返回用户信息
        User user = userService.getById(payload[0].toString());
        SsoUser ssoUser = new SsoUser();
        ssoUser.setUsername(user.getUsername());
        ssoUser.setNickname(user.getNickname());
        ssoUser.setAvatar(user.getAvatar());
        ssoUser.setType(user.getUserType());
        ssoUser.setDeviceType(DataUtil.toInteger(payload[1]));
        return ssoUser;
    }

    @Override
    public void logout(SsoNotifyServerLogout user) {
        String username = user.getUsername();
        AssertUtil.notEmpty(username, "缺少username");
        // 验证token
        Long clientId = parseToken(user.getSsoToken());
        // 退出登录
        if (user.getDeviceType() != null) {
            loginService.logout(user.getUsername(), RequestUtils.toDeviceType(user.getDeviceType()));
        } else {
            loginService.logout(user.getUsername());
        }
        // 查询所有在线的客户端
        List<SsoApply> list = clientService.list(
                Wrappers.<SsoApply>lambdaQuery()
                        .select(SsoApply::getSecret)
                        .eq(SsoApply::getStatus, DeviceStatusEnum.normal.getValue())
                        .ne(SsoApply::getId, clientId)
        );
        SsoUserBase userBase = new SsoUserBase();
        userBase.setDeviceType(user.getDeviceType());
        userBase.setUsername(user.getUsername());
        // 通知客户端用户下线
        list.forEach(x -> notifyClientLogout(x, userBase));
    }

    @Override
    public void offline(String token) {
        // 验证token
        Long clientId = parseToken(token);
        // 更新客户端状态
        clientService.update(
                Wrappers.<SsoApply>lambdaUpdate()
                        .set(SsoApply::getStatus, DeviceStatusEnum.offline.getValue())
                        .eq(SsoApply::getId, clientId)
        );
        LOGGER.info("客户端下线：" + clientId);
    }

    @Override
    public String getToken(Long clientId) {
        return soarJWT.create(clientId);
    }

    @Override
    public Long parseToken(String token) {
        SoarJWT.Result result = soarJWT.verify(token);
        AssertUtil.isTrue(result.isValid(), "sso token 无效");
        return Long.valueOf(result.getPayload(String.class));
    }

    @Override
    public void notifyAllClientLogout(SsoUserBase user) {
        // 查询所有在线的客户端
        List<SsoApply> list = clientService.list(
                Wrappers.<SsoApply>lambdaQuery()
                        .select(SsoApply::getSecret)
                        .eq(SsoApply::getStatus, DeviceStatusEnum.normal.getValue())
        );
        // 通知客户端用户下线
        list.forEach(x -> notifyClientLogout(x, user));
    }

    @Override
    public void notifyClientLogout(Long clientId, SsoUserBase user) {
        SsoApply client = clientService.getById(clientId);
        notifyClientLogout(client, user);
    }

    @Override
    public void notifyClientLogout(SsoApply client, SsoUserBase user) {
        String logoutNotifyUrl = client.getLogoutNotifyUrl();
        if (StringUtil.isBlank(logoutNotifyUrl)) {
            return;
        }
        // 发起通知
        SsoNotifyClientLogout notifyClient = new SsoNotifyClientLogout();
        notifyClient.setServerId(client.getOnlineId());
        notifyClient.setDeviceType(user.getDeviceType());
        notifyClient.setUsername(user.getUsername());
        notifyClient.signature(client.getSecret());
        String url = UrlUtils.handlerParams(logoutNotifyUrl, notifyClient);
        try {
            restTemplate.getForEntity(url, Void.class);
        } catch (RuntimeException e) {
            LOGGER.error("通知客户端用户下线失败: " + notifyClient, e);
        }
    }

}
